package tech.waterism.algorithm.moea.GA_SEPT;

import cn.hutool.extra.spring.SpringUtil;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import tech.waterism.constant.StringConstants;
import tech.waterism.dto.*;
import tech.waterism.utils.ModelPlatformUtils;

import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

public class FitnessCalculationSept {

    private final ModelPlatformUtils modelPlatformUtils;
    private final CalibrationMoeaDto dto;
    private final double[] variables;
    private final String[] types;
    private final String[] objectives;
    private final List<Integer> cutIndexList;
    private final double F;
    //
    private ForecastAndObserve forecastAndObserve;

    public FitnessCalculationSept(CalibrationMoeaDto dto, double[] variables, String[] types, String[] objectives) {
        modelPlatformUtils = SpringUtil.getBean("modelPlatformUtils");
        this.dto = dto;
        this.variables = variables;
        this.types = types;
        this.objectives = objectives;
        this.cutIndexList =
                MoeaCalculateSept.countDataNum(dto.getInterval(), dto.getFpIndex().getClen(), dto.getFpIndex().getUnt(),
                        dto.getPrdt());
        List<CalibrationMoeaDto> indexDataSept =
                MoeaCalculateSept.generSeptData(dto.getFpIndex(), dto.getFpInputs(), cutIndexList);
        for (int cutIndexListI = 0; cutIndexListI < cutIndexList.size(); cutIndexListI++) {
            List<FPInputDto> fpInputs = indexDataSept.get(cutIndexListI).getFpInputs();
            for (int inputIndex = 0; inputIndex < fpInputs.size(); inputIndex++) {
                FPInputDto fpInputDto = fpInputs.get(inputIndex);
                for (int fmIndexIndex = 0; fmIndexIndex < fpInputDto.getMdcdList().size(); fmIndexIndex++) {
                    FMIndexDto fmIndexDto = fpInputDto.getMdcdList().get(fmIndexIndex);
                    fmIndexDto.setStatuses(
                            dto.getIndexDataSept().get(cutIndexListI).getFpInputs().get(inputIndex).getMdcdList()
                                    .get(fmIndexIndex).getStatuses());
                }
            }
        }
        dto.setIndexDataSept(indexDataSept);
        // get total area
        double F = 0;
        for (FPInputDto fpInput : dto.getFpInputs()) {
            for (FMIndexDto fmIndexDto : fpInput.getMdcdList()) {
                for (FMParameterDto extraParameter : fmIndexDto.getExtraParameters()) {
                    if (extraParameter.getParameter().trim().startsWith("F")) {
                        String clearPara = extraParameter.getParameter().replace(" ", "");
                        if (clearPara.startsWith("F=")) {
                            F += Double.parseDouble(extraParameter.getParameter().trim().split("=")[1]);
                        }
                    }
                }
            }
        }
        if (F == 0) {
            F = 1;
        }
        this.F = F;
    }

    public double[] getFitness() throws Exception {
        dto.getFpIndex().getData().forEach(i -> i.setQSim(null));
        dto.getIndexDataSept().forEach(i -> i.getFpIndex().getData().forEach(ii -> ii.setQSim(null)));
        caliModelRun();
        return getStatistics();
    }

    /**
     * require:
     * forecastAndObserve
     * objectives
     */
    public double[] getStatistics() {
        double[] fitness = new double[objectives.length];
        double[][] fitnessTotal = new double[dto.getInterval().size()][objectives.length];
        for (int j = 0; j < dto.getInterval().size(); j++) {
            double[] observeSec = Arrays.copyOfRange(forecastAndObserve.getObserve(),
                    j == 0 ? 0 : forecastAndObserve.getIndexList().get(j - 1),
                    forecastAndObserve.getIndexList().get(j));
            double[] forecastSec = Arrays.copyOfRange(forecastAndObserve.getForecast(),
                    j == 0 ? 0 : forecastAndObserve.getIndexList().get(j - 1),
                    forecastAndObserve.getIndexList().get(j));
            for (int i = 0; i < fitness.length; i++) {
                switch (objectives[i]) {
                    case StringConstants.FLOOD_VOLUME_ERROR:
                        double RunoffDepthObs =
                                sumValue(observeSec) * forecastAndObserve.getClen() * 3.6 / forecastAndObserve.getF();
                        if (RunoffDepthObs == 0) {
                            RunoffDepthObs = 0.01;
                        }
                        double RunoffDepthFore =
                                sumValue(forecastSec) * forecastAndObserve.getClen() * 3.6 / forecastAndObserve.getF();
                        double RunoffAbsoluteError = Math.abs(RunoffDepthObs - RunoffDepthFore);
                        double RunoffRelativeError = RunoffAbsoluteError / RunoffDepthObs;
                        fitnessTotal[j][i] = RunoffRelativeError;
                        break;
                    case StringConstants.FLOOD_PEAK_ERROR:
                        double PeakObs = maxValue(observeSec);
                        if (PeakObs == 0) {
                            PeakObs = 0.01;
                        }
                        double PeakFore = maxValue(forecastSec);
                        double PeakAbsoluteError = Math.abs(PeakObs - PeakFore);
                        double PeakRelativeError = PeakAbsoluteError / PeakObs;
                        fitnessTotal[j][i] = PeakRelativeError;
                        break;
                    case StringConstants.PEAK_TIME_ERROR:
                        double PeakTimeObs = maxIndex(observeSec) + 1;
                        double PeakTimeFore = maxIndex(forecastSec) + 1;
                        double PeakTimeAbsoluteError =
                                Math.abs(PeakTimeObs - PeakTimeFore) * forecastAndObserve.getClen();
                        double PeakTimeRelativeError =
                                PeakTimeAbsoluteError / (PeakTimeObs * forecastAndObserve.getClen());
                        fitnessTotal[j][i] = PeakTimeRelativeError;
                        break;
                    case StringConstants.COEFFICIENT_OF_DETERMINATION:
                        double coefficientOfDetermination =
                                getNashCoefficient(forecastAndObserve.getObserve(), forecastAndObserve.getForecast());
                        fitnessTotal[j][i] = 1 - coefficientOfDetermination;
                        break;
                    case StringConstants.COEFFICIENT_OF_DETERMINATION_SINGLE:
                        double coefficientOfDeterminationSingle = getNashCoefficient(observeSec, forecastSec);
                        fitnessTotal[j][i] = 1 - coefficientOfDeterminationSingle;
                        break;
                }
            }
        }
        for (int i = 0; i < fitnessTotal.length; i++) {
            for (int j = 0; j < fitnessTotal[i].length; j++) {
                if (objectives[j].equals(StringConstants.COEFFICIENT_OF_DETERMINATION)) {
                    fitness[j] = fitnessTotal[0][j];
                    continue;
                }
                fitness[j] += fitnessTotal[i][j] / fitnessTotal.length;
            }
        }
        return fitness;
    }

    public void caliModelRun() throws Exception {
        // get obeseve value and forecast value
        forecastMoea();
        // statistics interval
        List<Double> qforeList = new LinkedList<>();
        List<Double> qrealList = new LinkedList<>();
        List<DateInterval> intervalList = dto.getInterval();
        List<Integer> indexList = new LinkedList<>();
        int tempIndex;
        for (int ii = 0; ii < dto.getIndexDataSept().size(); ii++) {
            CalibrationMoeaDto calibrationMoeaDto = dto.getIndexDataSept().get(ii);
            DateInterval interval = intervalList.get(ii);
            Date symdh = interval.getStart();
            Date eymdh = interval.getEnd();
            List<ForecastDataDto> statList = calibrationMoeaDto.getFpIndex().getData().stream()
                    .filter(i -> !i.getDt().before(symdh) && !i.getDt().after(eymdh)).toList();
            for (ForecastDataDto forecastDataDto : statList) {
                if (forecastDataDto.getQReal() != null && forecastDataDto.getQSim() != null) {
                    qforeList.add(forecastDataDto.getQSim());
                    qrealList.add(forecastDataDto.getQReal());
                }
            }
            tempIndex = qrealList.size();
            indexList.add(tempIndex);
        }
        double[] qfores = new double[qforeList.size()];
        double[] qreals = new double[qrealList.size()];
        for (int i = 0; i < qforeList.size(); i++) {
            qfores[i] = qforeList.get(i);
            qreals[i] = qrealList.get(i);
        }
        forecastAndObserve = new ForecastAndObserve(qfores, qreals, dto.getFpIndex().getClen(), F, indexList);
    }

    public void forecastModelRun() throws Exception {
        // get obeseve value and forecast value
        forecast();
        // statistics interval
        List<Double> qforeList = new LinkedList<>();
        List<Double> qrealList = new LinkedList<>();
        List<DateInterval> intervalList = dto.getInterval();
        List<Integer> indexList = new LinkedList<>();
        int tempIndex;
        for (int ii = 0; ii < dto.getIndexDataSept().size(); ii++) {
            CalibrationMoeaDto calibrationMoeaDto = dto.getIndexDataSept().get(ii);
            DateInterval interval = intervalList.get(ii);
            Date symdh = interval.getStart();
            Date eymdh = interval.getEnd();
            List<ForecastDataDto> statList = calibrationMoeaDto.getFpIndex().getData().stream()
                    .filter(i -> !i.getDt().before(symdh) && !i.getDt().after(eymdh)).toList();
            for (ForecastDataDto forecastDataDto : statList) {
                if (forecastDataDto.getQReal() != null && forecastDataDto.getQSim() != null) {
                    qforeList.add(forecastDataDto.getQSim());
                    qrealList.add(forecastDataDto.getQReal());
                }
            }
            tempIndex = qrealList.size();
            indexList.add(tempIndex);
        }
        double[] qfores = new double[qforeList.size()];
        double[] qreals = new double[qrealList.size()];
        for (int i = 0; i < qforeList.size(); i++) {
            qfores[i] = qforeList.get(i);
            qreals[i] = qrealList.get(i);
        }
        forecastAndObserve = new ForecastAndObserve(qfores, qreals, dto.getFpIndex().getClen(), F, indexList);
    }

    public CalibrationMoeaDto getForcast() {
        return dto;
    }

    public void forecastMoea() throws Exception {
        FPIndexDto fpIndex = dto.getFpIndex();
        Integer clen = fpIndex.getClen();
        for (CalibrationMoeaDto calibrationMoeaDto : dto.getIndexDataSept()) {
            List<FPInputDto> fpInputsSept = calibrationMoeaDto.getFpInputs();
            FPIndexDto fpIndexSept = calibrationMoeaDto.getFpIndex();
            List<ForecastDataDto> fpIndexSeptData = fpIndexSept.getData();
            int index = 0;
            for (FPInputDto fpInputDto : fpInputsSept) {
                for (FMIndexDto fmIndexDto : fpInputDto.getMdcdList()) {
                    if (fmIndexDto.getCaliParascopes() == null) {
                        ModelTestDto modelTestDto = new ModelTestDto();
                        BeanUtils.copyProperties(fmIndexDto, modelTestDto);
                        modelPlatformUtils.callModelForecast(modelTestDto, fpInputDto.getData(), clen, fpIndex.getUnt(),
                                fpInputDto.getData().get(0).getDt(), fpInputDto.getData().get(0).getDt(),
                                fpInputDto.getArcd(), fpInputDto.getArtp(), false, null, true);
                        fmIndexDto.setStatusesEnd(modelTestDto.getStatusesEnd());
                    } else {
                        int paramNum = fmIndexDto.getCaliParascopes().size();
                        double[] valueList = new double[paramNum];
                        String[] typeList = new String[paramNum];
                        for (int i = 0; i < paramNum; i++, index++) {
                            valueList[i] = variables[index];
                            typeList[i] = types[index];
                        }
                        modelPlatformUtils.callModelCalibrateMoea(fmIndexDto, valueList, typeList, fpInputDto.getData(),
                                clen, fpIndex.getUnt(), fpInputDto.getArnm(), fpInputDto.getArtp(), true);
                    }
                }
                for (int i = 0; i < fpInputDto.getData().size(); i++) {
                    // "qsim": 对应位置累加
                    ForecastDataDto forecastDataDto = fpInputDto.getData().get(i);
                    if (forecastDataDto.getQSim() != null) {
                        Double foreQsim = forecastDataDto.getQSim();
                        Double oriQsim =
                                fpIndexSeptData.get(i).getQSim() == null ? 0 : fpIndexSeptData.get(i).getQSim();
                        fpIndexSeptData.get(i).setQSim(foreQsim + oriQsim);
                    }
                }
            }
        }
    }

    /**
     * require:
     * dto
     */
    public void forecast() throws Exception {
        FPIndexDto fpIndex = dto.getFpIndex();
        Integer clen = fpIndex.getClen();
        for (CalibrationMoeaDto calibrationMoeaDto : dto.getIndexDataSept()) {
            List<FPInputDto> fpInputsSept = calibrationMoeaDto.getFpInputs();
            FPIndexDto fpIndexSept = calibrationMoeaDto.getFpIndex();
            fpIndexSept.getData().forEach(i -> i.setQSim(null));
            List<ForecastDataDto> fpIndexSeptData = fpIndexSept.getData();
            int index = 0;
            for (FPInputDto fpInputDto : fpInputsSept) {
                fpInputDto.getData().forEach(i -> i.setQSim(null));
                fpInputDto.getData().forEach(i -> i.setZSim(null));
            }
            for (FPInputDto fpInputDto : fpInputsSept) {
                for (FMIndexDto fmIndexDto : fpInputDto.getMdcdList()) {
                    ModelTestDto modelTestDto = new ModelTestDto();
                    BeanUtils.copyProperties(fmIndexDto, modelTestDto);
                    modelPlatformUtils.callModelForecast(modelTestDto, fpInputDto.getData(), clen, fpIndex.getUnt(),
                            fpInputDto.getData().get(0).getDt(), fpInputDto.getData().get(0).getDt(),
                            fpInputDto.getArcd(), fpInputDto.getArtp(), false, null, true);
                    fmIndexDto.setStatusesEnd(modelTestDto.getStatusesEnd());
                }
                for (int i = 0; i < fpInputDto.getData().size(); i++) {
                    // "qsim": 对应位置累加
                    ForecastDataDto forecastDataDto = fpInputDto.getData().get(i);
                    if (forecastDataDto.getQSim() != null) {
                        Double foreQsim = forecastDataDto.getQSim();
                        Double oriQsim =
                                fpIndexSeptData.get(i).getQSim() == null ? 0 : fpIndexSeptData.get(i).getQSim();
                        fpIndexSeptData.get(i).setQSim(foreQsim + oriQsim);
                    }
                }
            }
        }
    }

    // 确定性系数
    private double getNashCoefficient(double[] valueObs, double[] valueFore) {
        double DC = 0.0;
        double a = 0.0;
        double b = 0.0;
        double obsAvg = 0.0;
        for (int i = 0; i < valueObs.length; i++) {
            obsAvg += valueObs[i] / valueObs.length;
        }
        for (int i = 0; i < valueFore.length; i++) {
            a += Math.pow((valueObs[i] - valueFore[i]), 2);
            b += Math.pow((valueObs[i] - obsAvg), 2);
        }
        if (b == 0) {
            b = 0.01;
        }
        DC = 1 - a / b;

        return DC;
    }

    private double sumValue(double[] q) {
        double sum = 0;
        for (double v : q) {
            sum += v;
        }
        return sum;
    }

    private double maxValue(double[] q) {
        double max = q[0];
        for (int i = 1; i < q.length; i++) {
            if (q[i] > max) {
                max = q[i];
            }
        }
        return max;
    }

    private int maxIndex(double[] q) {
        double max = q[0];
        int index = 0;
        for (int i = 1; i < q.length; i++) {
            if (q[i] > max) {
                max = q[i];
                index = i;
            }
        }
        return index;
    }

    @SneakyThrows
    public void updateNewStatus(CalibrationMoeaDto result) {
        Date statusDate = result.getInterval().stream().map(DateInterval::getStart).max(Date::compareTo).get();
        List<FPInputDto> fpInputs = result.getFpInputs();
        for (FPInputDto fpInputDto : fpInputs) {
            for (FMIndexDto fmIndexDto : fpInputDto.getMdcdList()) {
                ModelTestDto modelTestDto = new ModelTestDto();
                BeanUtils.copyProperties(fmIndexDto, modelTestDto);
                modelPlatformUtils.callModelForecast(modelTestDto, fpInputDto.getData(), result.getFpIndex().getClen(),
                        dto.getFpIndex().getUnt(), statusDate, statusDate, fpInputDto.getArcd(), fpInputDto.getArtp(),
                        false);
            }
        }
    }
}
